/*
 * Copyright 2015 Netflix, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.mes.cloud.base.feign;

import com.alibaba.fastjson.JSON;
import com.mes.cloud.base.ErrorNotifier;
import com.mes.cloud.base.ServiceInfo;
import com.mes.cloud.base.cache.LocalCache;
import com.mes.cloud.base.cache.LocalCacheService;
import com.mes.cloud.commons.Constants;
import com.mes.cloud.utils.JSONResult;
import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
import com.netflix.hystrix.HystrixCommandKey;
import feign.InvocationHandlerFactory.MethodHandler;
import feign.Target;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.util.ReflectionUtils;
import rx.Observable;
import rx.Single;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Map;

import static feign.Util.checkNotNull;

public class FeignInvocationHandler implements InvocationHandler {
    public final static Logger logger = LoggerFactory.getLogger(FeignInvocationHandler.class);

    private final ServiceInfo serviceInfo;
    private final ErrorNotifier notifier;
    private final Target<?> target;
    private final Map<Method, MethodHandler> dispatch;
    private final Object testHandler;
    private final LocalCacheService localCacheService;


    FeignInvocationHandler(BeanFactory beanFactory, boolean inTest, Target<?> target, Map<Method, MethodHandler> dispatch) {
        this.serviceInfo = beanFactory.getBean(ServiceInfo.class);
        this.notifier = beanFactory.getBean(ErrorNotifier.class);
        this.target = checkNotNull(target, "target");
        this.dispatch = checkNotNull(dispatch, "dispatch");

        Object testHandler = null;

        if (inTest) {
            String serviceName = "fallback" + target.type().getSimpleName();
            try {
                testHandler = beanFactory.getBean(serviceName);
            } catch (Throwable e) {
                //ignored
            }
        }
        this.testHandler = testHandler;

        LocalCacheService cacheService = null;
        try {
            cacheService = beanFactory.getBean(LocalCacheService.class);
        } catch (Throwable e) {
            logger.error("Unable to get LocalCacheService bean.", e);
        }
        this.localCacheService = cacheService;
    }

    @Override
    public Object invoke(final Object proxy, final Method method, final Object[] args)
            throws Throwable {
        // early exit if the invoked method is from java.lang.Object
        // code is the same as ReflectiveFeign.FeignInvocationHandler
        if ("equals".equals(method.getName())) {
            try {
                Object otherHandler =
                        args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
                return equals(otherHandler);
            } catch (IllegalArgumentException e) {
                return false;
            }
        } else if ("hashCode".equals(method.getName())) {
            return hashCode();
        } else if ("toString".equals(method.getName())) {
            return toString();
        }

        if (localCacheService != null && method.isAnnotationPresent(LocalCache.class)) {
            return localCacheService.execute(method, args, () -> invokeForCache(method, args));
        }
        return doInvoke(method, args);
    }

    private LocalCacheService.ExecuteResult invokeForCache(Method method, Object[] args) throws Throwable {
        Object object = doInvoke(method, args);
        boolean willCached = object != null && JSONResult.class.isInstance(object)
                && ((JSONResult) object).isState();
        return new LocalCacheService.ExecuteResult(object, willCached);
    }

    private Object doInvoke(final Method method, final Object[] args) throws Throwable {
        String groupKey = this.target.name();
        String commandKey = method.getName();
        HystrixCommand.Setter setter = HystrixCommand.Setter
                .withGroupKey(HystrixCommandGroupKey.Factory.asKey(groupKey))
                .andCommandKey(HystrixCommandKey.Factory.asKey(commandKey));

        HystrixCommand<Object> hystrixCommand = new HystrixCommand<Object>(setter) {
            @Override
            protected Object run() throws Exception {
                try {
                    Object result = testHandler != null ? handleTest(method, args) : null;
                    if (result == null) {
                        result = FeignInvocationHandler.this.dispatch.get(method).invoke(args);
                    }
                    return result;
                } catch (Exception e) {
                    throw e;
                } catch (Throwable t) {
                    throw (Error) t;
                }
            }

            @Override
            protected Object getFallback() {
                return handleError(method, args, getExecutionException());
            }
        };

        if (isReturnsHystrixCommand(method)) {
            return hystrixCommand;
        } else if (isReturnsObservable(method)) {
            // Create a cold Observable
            return hystrixCommand.toObservable();
        } else if (isReturnsSingle(method)) {
            // Create a cold Observable as a Single
            return hystrixCommand.toObservable().toSingle();
        }
        return hystrixCommand.execute();
    }

    private boolean isReturnsSingle(Method method) {
        return Single.class.isAssignableFrom(method.getReturnType());
    }

    private boolean isReturnsHystrixCommand(Method method) {
        return HystrixCommand.class.isAssignableFrom(method.getReturnType());
    }

    private boolean isReturnsObservable(Method method) {
        return Observable.class.isAssignableFrom(method.getReturnType());
    }

    private JSONResult handleTest(Method method, Object[] args) throws Exception {
        Method targetMethod = ReflectionUtils.findMethod(testHandler.getClass(), method.getName(),
                method.getParameterTypes());
        if (targetMethod != null) {
            targetMethod.setAccessible(true);
            return (JSONResult) targetMethod.invoke(testHandler, args);
        }
        return null;
    }

    private JSONResult handleError(Method method, Object[] args, Throwable throwable) {
        String targetApplicationName = target.name();
        String className = target.type().getName();
        String methodName = method.getName();

        String message = String.format("服务调用失败.From:[%s], To:[%s], Method:[%s#%s]",
                serviceInfo, targetApplicationName, className, methodName);

        while (throwable != null && RuntimeException.class.equals(throwable.getClass()) && throwable.getCause() != null) {
            throwable = throwable.getCause();
        }

        logger.error(message, throwable);

        //notifier.notifyByMobile(throwable, message);

        notifier.notifyByMail(throwable, "%s<br/><br/>Arguments:%s", message, JSON.toJSONString(args));

        JSONResult result = Constants.Errors.FEIGN_REQUEST_ERROR;
        return result.appendMessage(getBriefThrowableMessage(throwable));
    }

    private String getBriefThrowableMessage(Throwable throwable) {
        return throwable == null ? "UNKNOWN ERROR." : throwable.getClass().getSimpleName();
        //String.format("%s: %s", throwable.getClass().getSimpleName(), throwable.getMessage());
    }
}